// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Web.Security;

namespace BlueOnionSoftware.Bloget
{
    internal static class MetaWebLog
    {
        internal const string MethodNewPost = "metaWeblog.newPost";
        internal const string MethodEditPost = "metaWeblog.editPost";
        internal const string MethodGetPost = "metaWeblog.getPost";
        internal const string MethodGetRecentPosts = "metaWeblog.getRecentPosts";
        internal const string MethodGetCategories = "metaWeblog.getCategories";
        internal const string MethodNewMediaObject = "metaWeblog.newMediaObject";

        private const string InvalidNamePassword = "Invalid username or password";

        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        internal static XmlRpcMethodResponse Post(XmlRpcMethodCall methodCall, Bloget bloget)
        {
            Throw.IfNull(methodCall, "methodCall");
            Throw.IfNull(bloget, "bloget");

            if (methodCall.Name != MethodNewPost && methodCall.Name != MethodEditPost)
            {
                throw new ArgumentException("MetaWebLog.Post - method call is " + methodCall.Name);
            }

            if (bloget.Blog.EnableMetaWeblogApi == false)
            {
                return MetaWebLogNotEnabled();
            }

            var isNewPost = (methodCall.Name == MethodNewPost);
            var parameters = methodCall.Parameters;
            var response = new XmlRpcMethodResponse();

            if (parameters.Length != 5)
            {
                response.FaultString = methodCall.Name + " requires 5 parameters";
                return response;
            }

            var postId = parameters[0].Value as string;
            var userName = parameters[1].Value as string;
            var password = DecodePasswordParameter(parameters[2].Value);
            var posting = parameters[3].Value as XmlRpcStruct;

            if (posting == null)
            {
                throw new InvalidCastException();
            }

            var publish = (bool) parameters[4].Value;

            if (IsValidUserPassword(userName, password) == false)
            {
                response.FaultString = InvalidNamePassword;
                return response;
            }

            var blog = new Blog(bloget.Blog); // copy to protect readers
            var post = new Post();
            var content = new Content();

            if (isNewPost == false)
            {
                var id = new Guid(postId);

                if (blog.Posts.Contains(id) == false)
                {
                    response.FaultString = "Post ID not found";
                    return response;
                }

                post = blog.Posts[id];
                content = blog.LoadContent(id);
                content = new Content(content); // Clone to protect readers
            }

            post.Draft = !publish;

            foreach (var member in posting.Members)
            {
                switch (member.Name)
                {
                    case "title":
                        post.Title = member.Value.Value.ToString();
                        break;

                    case "description":
                        content.Text = member.Value.Value.ToString();
                        break;

                    case "categories":
                        var categories = member.Value.Value as XmlRpcArray;

                        if (categories != null)
                        {
                            foreach (var categoryValue in categories.Values)
                            {
                                var category = blog.Categories.GetCategoryByName(categoryValue.Value.ToString());

                                if (category != null)
                                {
                                    post.AddCategory(category);
                                }
                            }
                        }
                        break;

                    case "dateCreated":
                        post.Created = (DateTime) member.Value.Value;
                        break;

                    case "source":
                        ReadSourceStruct(member.Value.Value as XmlRpcStruct, content);
                        break;

                    case "enclosure":
                        var attachment = ReadEnclosureStruct(member.Value.Value as XmlRpcStruct);
                        content.Attachments.Clear();

                        if (string.IsNullOrEmpty(attachment.Url) == false &&
                            string.IsNullOrEmpty(attachment.MimeType) == false &&
                                attachment.Length != 0)
                        {
                            content.Attachments.Add(attachment);
                        }
                        break;

                    default:
                        // ignore everything else
                        break;
                }
            }

            if (string.IsNullOrEmpty(post.Title) || string.IsNullOrEmpty(content.Text))
            {
                response.FaultString = "Posting requires a title and description";
                return response;
            }

            blog.SaveContent(post.Id, content);

            if (isNewPost)
            {
                blog.Posts.InsertByDate(post);
                Log.NewPost(bloget, post.Id, post.Title);
            }

            else
            {
                Log.EditPost(bloget, post.Id, post.Title);
            }

            blog.Save();

            if (blog.EnableAutoPingBack && post.Draft == false)
            {
                PingBackClient.PingLinks(bloget, bloget.PermaLink(post), content.Text);
            }

            var success = new XmlRpcValue(post.Id.ToString());
            response.AddParam(success);
            return response;
        }

        internal static XmlRpcMethodResponse GetPost(XmlRpcMethodCall methodCall, Bloget bloget)
        {
            Throw.IfNull(methodCall, "methodCall");
            Throw.IfNull(bloget, "bloget");

            if (methodCall.Name != MethodGetPost)
            {
                throw new ArgumentException("MetaWebLog.GetPost - method call is " + methodCall.Name);
            }

            if (bloget.Blog.EnableMetaWeblogApi == false)
            {
                return MetaWebLogNotEnabled();
            }

            var parameters = methodCall.Parameters;
            var response = new XmlRpcMethodResponse();

            if (parameters.Length != 3)
            {
                response.FaultString = "metaWeblog.getPost requires 3 parameters";
                return response;
            }

            var postId = parameters[0].Value as string;
            var userName = parameters[1].Value as string;
            var password = DecodePasswordParameter(parameters[2].Value);

            if (IsValidUserPassword(userName, password) == false)
            {
                response.FaultString = InvalidNamePassword;
                return response;
            }

            var id = new Guid(postId);

            if (bloget.Blog.Posts.Contains(id) == false)
            {
                response.FaultString = "Posting ID not found";
                return response;
            }

            var post = bloget.Blog.Posts[id];
            var content = bloget.Blog.LoadContent(id);
            response.AddParam(new XmlRpcValue(PostStruct(bloget, post, content)));
            Log.GetPost(bloget, id);
            return response;
        }

        internal static XmlRpcMethodResponse GetRecentPosts(XmlRpcMethodCall methodCall, Bloget bloget)
        {
            Throw.IfNull(methodCall, "methodCall");
            Throw.IfNull(bloget, "bloget");

            if (methodCall.Name != MethodGetRecentPosts)
            {
                throw new ArgumentException("MetaWebLog.GetRecentPosts - method call is " + methodCall.Name);
            }

            if (bloget.Blog.EnableMetaWeblogApi == false)
            {
                return MetaWebLogNotEnabled();
            }

            var parameters = methodCall.Parameters;
            var response = new XmlRpcMethodResponse();

            if (parameters.Length != 4)
            {
                response.FaultString = "metaWeblog.GetRecentPosts requires 4 parameters";
                return response;
            }

            var userName = parameters[1].Value as string;
            var password = DecodePasswordParameter(parameters[2].Value);
            var numberOfPosts = (int) parameters[3].Value;

            if (IsValidUserPassword(userName, password) == false)
            {
                response.FaultString = InvalidNamePassword;
                return response;
            }

            var array = new XmlRpcArray();
            numberOfPosts = Math.Min(numberOfPosts, bloget.Blog.Posts.Count);

            for (var i = 0; i < numberOfPosts; ++i)
            {
                var post = bloget.Blog.Posts[i];
                var content = bloget.Blog.LoadContent(post.Id);
                array.Add(new XmlRpcValue(PostStruct(bloget, post, content)));
            }

            response.AddParam(new XmlRpcValue(array));
            return response;
        }

        internal static XmlRpcMethodResponse GetCategories(XmlRpcMethodCall methodCall, Bloget bloget)
        {
            Throw.IfNull(methodCall, "methodCall");
            Throw.IfNull(bloget, "bloget");

            if (methodCall.Name != MethodGetCategories)
            {
                throw new ArgumentException("MetaWebLog.GetCategories - method call is " + methodCall.Name);
            }

            if (bloget.Blog.EnableMetaWeblogApi == false)
            {
                return MetaWebLogNotEnabled();
            }

            var parameters = methodCall.Parameters;
            var response = new XmlRpcMethodResponse();

            if (parameters.Length != 3)
            {
                response.FaultString = "metaWeblog.getCategories requires 3 parameters";
                return response;
            }

            // blogId not used
            var userName = parameters[1].Value as string;
            var password = DecodePasswordParameter(parameters[2].Value);

            if (IsValidUserPassword(userName, password) == false)
            {
                response.FaultString = InvalidNamePassword;
                return response;
            }

            var array = new XmlRpcArray();

            foreach (var category in bloget.Blog.Categories)
            {
                var categoryStruct = new XmlRpcStruct();
                categoryStruct.Add(new XmlRpcMember("title", category.Name));
                categoryStruct.Add(new XmlRpcMember("categoryid", category.Id.ToString(CultureInfo.CurrentCulture)));
                categoryStruct.Add(new XmlRpcMember("description", category.Name));
                array.Add(new XmlRpcValue(categoryStruct));
            }

            response.AddParam(new XmlRpcValue(array));
            return response;
        }

        internal static XmlRpcMethodResponse NewMediaObject(XmlRpcMethodCall methodCall, Bloget bloget)
        {
            Throw.IfNull(methodCall, "methodCall");
            Throw.IfNull(bloget, "bloget");

            if (methodCall.Name != MethodNewMediaObject)
            {
                throw new ArgumentException("MetaWebLog.NewMediaObject - method call is " + methodCall.Name);
            }

            if (bloget.Blog.EnableMetaWeblogApi == false || string.IsNullOrEmpty(bloget.Blog.ImageFolder))
            {
                return MetaWebLogNotEnabled();
            }

            var parameters = methodCall.Parameters;
            var response = new XmlRpcMethodResponse();

            if (parameters.Length != 4)
            {
                response.FaultString = "metaWeblog.newMediaObject requires 4 parameters";
                return response;
            }

            // blogId not used
            var userName = parameters[1].Value as string;
            var password = DecodePasswordParameter(parameters[2].Value);
            var media = parameters[3].Value as XmlRpcStruct;

            if (media == null)
            {
                throw new InvalidCastException();
            }

            if (IsValidUserPassword(userName, password) == false)
            {
                response.FaultString = InvalidNamePassword;
                return response;
            }

            string name = null;
            string type = null;
            byte[] bits = null;

            foreach (var member in media.Members)
            {
                switch (member.Name)
                {
                    case "name":
                        name = member.Value.ToString();
                        break;

                    case "type":
                        type = member.Value.ToString();
                        break;

                    case "bits":
                        bits = member.Value.Value as byte[];
                        break;

                    default:
                        // ignore everything else
                        break;
                }
            }

            if (name == null || bits == null) // some editors (like RocketPost) don't send the type
            {
                response.FaultString = "media type must contain a name and bits";
                return response;
            }

            if (name.StartsWith("/", StringComparison.Ordinal))
            {
                name = name.Substring(1);
            }

            var folder = bloget.Page.Request.MapPath(bloget.Blog.ImageFolder);
            var path = Path.Combine(folder, name);
            var directoryName = Path.GetDirectoryName(path);

            if (Directory.Exists(directoryName) == false)
            {
                Directory.CreateDirectory(directoryName);
            }

            File.WriteAllBytes(path, bits);

            var url = bloget.Blog.ImageUrl + name;

            var logMessage =
                string.Format(CultureInfo.InvariantCulture, "{0} (name={1}, type={2}, url={3})", MethodNewMediaObject,
                              name, type, url);

            Log.RpcRequest(bloget, logMessage);

            var structUrl = new XmlRpcStruct();
            structUrl.Add(new XmlRpcMember("url", url));
            response.AddParam(new XmlRpcValue(structUrl));

            return response;
        }

        internal static XmlRpcMethodResponse MetaWebLogNotEnabled()
        {
            var response = new XmlRpcMethodResponse {FaultString = "MetaWeblog Api not enabled"};
            return response;
        }

        internal static bool IsValidUserPassword(string username, string password)
        {
            var valid = Membership.ValidateUser(username, password);

            if (valid)
            {
                var user = Membership.GetUser(username);
                valid = Bloget.IsUserAdministrator(user) || Bloget.IsUserAuthor(user);
            }

            return valid;
        }

        private static void ReadSourceStruct(XmlRpcStruct sourceStruct, Content content)
        {
            Throw.IfNull(sourceStruct, "sourceStruct");
            Throw.IfNull(content, "content");

            foreach (var sourceMember in sourceStruct.Members)
            {
                switch (sourceMember.Name)
                {
                    case "url":
                        content.SourceLink = (string) sourceMember.Value.Value;
                        break;

                    case "name":
                        content.SourceTitle = (string) sourceMember.Value.Value;
                        break;

                    default:
                        break;
                }
            }

            return;
        }

        private static Attachment ReadEnclosureStruct(XmlRpcStruct enclosureStruct)
        {
            Throw.IfNull(enclosureStruct, "enclosureStruct");
            var attachment = new Attachment();

            foreach (var enclosureMember in enclosureStruct.Members)
            {
                switch (enclosureMember.Name)
                {
                    case "url":
                        attachment.Url = enclosureMember.Value.Value as string;
                        break;

                    case "type":
                        attachment.MimeType = enclosureMember.Value.Value as string;
                        break;

                    case "length":
                        attachment.Length = (int) enclosureMember.Value.Value;
                        break;

                    default:
                        break;
                }
            }

            return attachment;
        }

        private static XmlRpcStruct PostStruct(Bloget bloget, Post post, Content content)
        {
            var postStruct = new XmlRpcStruct();
            postStruct.Add(new XmlRpcMember("title", post.Title));
            postStruct.Add(new XmlRpcMember("description", content.Text));
            postStruct.Add(new XmlRpcMember("dateCreated", post.Created));
            postStruct.Add(new XmlRpcMember("link", bloget.PermaLink(post)));
            postStruct.Add(new XmlRpcMember("postid", post.Id.ToString()));

            if (string.IsNullOrEmpty(content.SourceTitle) == false && string.IsNullOrEmpty(content.SourceLink) == false)
            {
                var sourceStruct = new XmlRpcStruct();
                sourceStruct.Add(new XmlRpcMember("url", content.SourceLink));
                sourceStruct.Add(new XmlRpcMember("name", content.SourceTitle));
                postStruct.Add(new XmlRpcMember("source", sourceStruct));
            }

            return postStruct;
        }

        internal static string DecodePasswordParameter(object value)
        {
            // Some systems send passwords as base64 string

            var password = value as string;

            if (password == null)
            {
                var buffer = value as byte[];

                if (buffer != null)
                {
                    password = Encoding.UTF8.GetString(buffer);
                }
            }

            return password;
        }
    }
}